"""Convert Blender action into some intermedia class,
then serialized to godot escn"""

import re
import collections
import logging
import math
import copy
import bpy
import mathutils
from .serializer import FloatTrack, TransformTrack, ColorTrack, TransformFrame
from .constraint_baking import check_object_constraint
from ...structures import (NodePath, fix_bone_attachment_location)


# a triple contains information to convert an attribute
# or a fcurve of blender to godot data structures
AttributeConvertInfo = collections.namedtuple(
    'AttributeConvertInfo',
    ['bl_name', 'gd_name', 'converter_function']
)


def get_action_frame_range(action):
    """Return a tuple which is the frame range of action"""
    # in blender `last_frame` is included, here plus one to make it
    # excluded to fit python convention
    return int(action.frame_range[0]), int(action.frame_range[1]) + 1


def get_strip_frame_range(strip):
    """Return a tuple which is the frame of a NlaStrip"""
    return int(strip.frame_start), int(strip.frame_end) + 1


class ActionStrip:
    """Abstract of blender action strip, it may override attributes
    of an action object"""

    def __init__(self, action_or_strip):
        self.action = None
        self.frame_range = (0, 0)

        # blender strip does a linear transformation to its
        # wrapped action frame range, so we need a k, b
        # to store the linear function
        self._fk = 1
        self._fb = 0

        if isinstance(action_or_strip, bpy.types.NlaStrip):
            strip = action_or_strip
            self.action = strip.action
            self._fk = (
                (strip.frame_end - strip.frame_start) /
                (self.action.frame_range[1] - self.action.frame_range[0])
            )
            self._fb = self.action.frame_range[1] - self._fk * strip.frame_end
            self.frame_range = get_strip_frame_range(strip)
        elif isinstance(action_or_strip, bpy.types.Action):
            self.action = action_or_strip
            self.frame_range = get_action_frame_range(self.action)
        else:  # action_or_strip is None
            self.frame_range = (0, 190)

    def evaluate_fcurve(self, fcurve, frame):
        """Evaluate a value of fcurve, DO NOT use fcurve.evalute, as
        action may wrapped inside an action strip"""
        return fcurve.evaluate(self._fk * frame + self._fb)

    def evalute_keyframe(self, keyframe):
        """Evaluate a key frame point and return the point in tuple,
        DO NOT directly use keyframe.co, as action may wrapped in a strip"""
        return int(self._fk * keyframe.co[0] + self._fb), keyframe.co[1]


def blender_path_to_bone_name(blender_object_path):
    """Find the bone name inside a fcurve data path,
    the parameter blender_object_path is part of
    the fcurve.data_path generated through
    split_fcurve_data_path()"""
    return re.search(r'pose.bones\["([^"]+)"\]',
                     blender_object_path).group(1)


def split_fcurve_data_path(data_path):
    """Split fcurve data path into a blender
    object path and an attribute name"""
    path_list = data_path.rsplit('.', 1)

    if len(path_list) == 1:
        return '', path_list[0]
    return path_list[0], path_list[1]


def has_obj_fcurves(action_strip):
    """Check whether action has object transform information"""
    if action_strip.action is None:
        return False
    for fcurve in action_strip.action.fcurves:
        obj_path, attribute = split_fcurve_data_path(fcurve.data_path)
        if obj_path == '':
            return True
    return False


# pylint: disable-msg=too-many-locals
def export_constrained_xform_action(godot_node, export_settings,
                                    blender_object, action_strip,
                                    anim_rsc):
    """Export transform animation of any object has constraints,
    it use frame_set to traversal each frame, so it's costly"""
    def build_pbone_parent_map(godot_node, blender_object):
        pose_bone_parent_map = dict()
        for pbone in blender_object.pose.bones:
            pbone_parent = pbone.parent
            # find parent bone and ensure it is exported in godot node
            while (pbone_parent is not None and
                   godot_node.find_bone_id(pbone_parent.name) == -1):
                pbone_parent = pbone_parent.parent
            pose_bone_parent_map[pbone.name] = pbone_parent
        return pose_bone_parent_map

    first_frame, last_frame = action_strip.frame_range

    obj_xform_mats = list()
    pbone_xform_mats = collections.OrderedDict()

    scene = bpy.context.scene
    frame_backup = scene.frame_current

    has_pbone_actions = (godot_node.get_type() == 'Skeleton' and
                         blender_object.pose is not None)
    if has_pbone_actions:
        pbone_parent_map = build_pbone_parent_map(godot_node, blender_object)

    for frame in range(first_frame, last_frame):
        scene.frame_set(frame)
        obj_xform_mats.append(blender_object.matrix_local.copy())
        if has_pbone_actions:
            for pbone in blender_object.pose.bones:
                pbone_parent = pbone_parent_map[pbone.name]
                if pbone_parent is None:
                    bone_space_xform = (
                        pbone.bone.matrix_local.inverted_safe() @ pbone.matrix)
                else:
                    bone_space_xform = (
                        godot_node.find_bone_rest(pbone.name).inverted_safe() @
                        pbone_parent.matrix.inverted_safe() @
                        pbone.matrix)

                if pbone.name not in pbone_xform_mats:
                    pbone_xform_mats[pbone.name] = list()
                pbone_xform_mats[pbone.name].append(bone_space_xform)
    scene.frame_set(frame_backup)

    if (check_object_constraint(blender_object) or
            has_obj_fcurves(action_strip)):
        xform_frames_list = [
            TransformFrame.factory(mat)
            for mat in obj_xform_mats
        ]

        track_path = NodePath(
            anim_rsc.anim_player.parent.get_path(),
            godot_node.get_path()
        )

        if godot_node.parent.get_type() == 'BoneAttachment':
            xform_frames_list = [
                fix_bone_attachment_location(blender_object, x.location)
                for x in xform_frames_list
            ]

        anim_rsc.add_obj_xform_track(
            godot_node.get_type(), track_path,
            xform_frames_list, action_strip.frame_range,
            # no need for parent_inverse, as it is directly access matrix_local
        )

    if has_pbone_actions:
        for pbone_name, pbone_xform_mat_list in pbone_xform_mats.items():
            if godot_node.find_bone_id(pbone_name) != -1:
                pbone_xform_frames_list = [
                    TransformFrame.factory(mat)
                    for mat in pbone_xform_mat_list
                ]

                track_path = NodePath(
                    anim_rsc.anim_player.parent.get_path(),
                    godot_node.get_path(),
                    godot_node.find_bone_name(pbone_name),
                )

                anim_rsc.add_track(
                    TransformTrack(
                        track_path,
                        frames_iter=range(first_frame, last_frame),
                        values_iter=pbone_xform_frames_list,
                    )
                )


def export_transform_action(godot_node, export_settings, blender_object,
                            action_strip, anim_rsc):
    """Export a action with bone and object transform"""
    def init_transform_frames_list(object_path, blender_object, godot_node,
                                   first_frame, last_frame):
        """Initialize a list of TransformFrame for an animated object"""
        if object_path.startswith('pose'):
            bone_name = blender_path_to_bone_name(object_path)

            # bone fcurve in a non armature object
            if godot_node.get_type() != 'Skeleton':
                logging.warning(
                    "Skip bone fcurves of Armature object not being exported. "
                    "object '%s'",
                    blender_object.name
                )
                return None

            # if the correspond bone of this track not exported, skip
            if godot_node.find_bone_id(bone_name) == -1:
                return None

            pose_bone = blender_object.pose.bones[
                blender_object.pose.bones.find(bone_name)
            ]

            default_frame = TransformFrame.factory(
                pose_bone.matrix_basis,
                pose_bone.rotation_mode
            )
        else:
            # the fcurve location is matrix_basis.to_translation()
            default_frame = TransformFrame.factory(
                blender_object.matrix_basis,
                blender_object.rotation_mode
            )

        return [
            copy.deepcopy(default_frame)
            for _ in range(last_frame - first_frame)
        ]

    first_frame, last_frame = action_strip.frame_range
    xform_frames_list_map = collections.OrderedDict()
    for fcurve in action_strip.action.fcurves:
        # fcurve data are seperated into different channels,
        # for example a transform action would have several fcurves
        # (location.x, location.y, rotation.x ...), so here fcurves
        # are aggregated to object while being evaluted
        object_path, attribute = split_fcurve_data_path(fcurve.data_path)

        if attribute in TransformFrame.ATTRIBUTES:
            if object_path not in xform_frames_list_map:

                frame_values = init_transform_frames_list(
                    object_path, blender_object,
                    godot_node, first_frame, last_frame
                )

                # unsuccessfully initialize frames, then skip this fcurve
                if not frame_values:
                    continue

                xform_frames_list_map[object_path] = frame_values

            for frame in range(first_frame, last_frame):
                xform_frames_list_map[object_path][frame - first_frame].update(
                    attribute,
                    fcurve.array_index,
                    action_strip.evaluate_fcurve(fcurve, frame)
                )

    for object_path, frame_value_list in xform_frames_list_map.items():
        if object_path == '':
            # empty object_path represents transform of object itself
            track_path = NodePath(
                anim_rsc.anim_player.parent.get_path(),
                godot_node.get_path()
            )

            if godot_node.parent.get_type() == 'BoneAttachment':
                frame_value_list = [
                    fix_bone_attachment_location(blender_object, x.location)
                    for x in frame_value_list
                ]

            anim_rsc.add_obj_xform_track(
                godot_node.get_type(), track_path,
                frame_value_list, action_strip.frame_range,
                blender_object.matrix_parent_inverse
            )

        elif object_path.startswith('pose'):
            track_path = NodePath(
                anim_rsc.anim_player.parent.get_path(),
                godot_node.get_path(),
                godot_node.find_bone_name(
                    blender_path_to_bone_name(object_path)
                ),
            )
            anim_rsc.add_track(
                TransformTrack(
                    track_path,
                    frames_iter=range(first_frame, last_frame),
                    values_iter=frame_value_list,
                )
            )


def export_shapekey_action(godot_node, export_settings, blender_object,
                           action_strip, anim_rsc):
    """Export shapekey value action"""
    base_node_path = NodePath(
        anim_rsc.anim_player.parent.get_path(), godot_node.get_path()
    )
    for fcurve in action_strip.action.fcurves:
        object_path, attribute = split_fcurve_data_path(fcurve.data_path)

        if attribute == 'value':
            shapekey_name = re.search(r'key_blocks\["([^"]+)"\]',
                                      object_path).group(1)

            anim_rsc.add_attribute_track(
                action_strip,
                fcurve,
                lambda x: x,
                base_node_path.new_copy(
                    "blend_shapes/{}".format(shapekey_name)),
                use_bezier=export_settings['feature_bezier_track']
            )


def export_light_action(light_node, export_settings, blender_lamp,
                        action_strip, anim_rsc):
    """Export light(lamp in Blender) action"""
    # pylint: disable-msg=too-many-branches
    base_node_path = NodePath(
        anim_rsc.anim_player.parent.get_path(), light_node.get_path()
    )

    fcurves = action_strip.action.fcurves
    # CONSTANT tracks
    anim_rsc.add_attribute_track(
        action_strip,
        fcurves.find('shadow_method'),
        lambda x: x > 0.0,
        base_node_path.new_copy('shadow_enabled'),
    )

    color_attr_conversion = list()
    # simple value tracks
    for item in light_node.attribute_conversion:
        bl_attr, gd_attr, converter = item
        if bl_attr in ('color', 'shadow_color'):
            color_attr_conversion.append(item)
        else:
            anim_rsc.add_attribute_track(
                action_strip,
                fcurves.find(bl_attr),
                converter,
                base_node_path.new_copy(gd_attr),
                use_bezier=export_settings["feature_bezier_track"]
            )

    if export_settings["feature_bezier_track"]:
        rgba_names = ('r', 'g', 'b', 'a')
        for fcurve in fcurves:
            if fcurve is None:
                continue
            _, attribute = split_fcurve_data_path(fcurve.data_path)

            for bl_attr, gd_attr, converter in color_attr_conversion:
                if bl_attr == attribute:
                    track_path = base_node_path.new_copy(
                        gd_attr + ':' + rgba_names[fcurve.array_index]
                    )
                    anim_rsc.add_attribute_track(
                        action_strip,
                        fcurve,
                        converter,
                        track_path,
                        use_bezier=True)
    else:
        # color tracks is not one-one mapping to fcurve, they
        # need to be treated like transform track
        color_frame_values_map = collections.OrderedDict()

        first_frame, last_frame = action_strip.frame_range
        for fcurve in fcurves:
            _, attribute = split_fcurve_data_path(fcurve.data_path)

            if attribute in ('color', 'shadow_color'):
                if attribute not in color_frame_values_map:
                    color_frame_values_map[attribute] = [
                        mathutils.Color()
                        for _ in range(first_frame, last_frame)
                    ]
                color_list = color_frame_values_map[attribute]
                for frame in range(first_frame, last_frame):
                    color_list[frame - first_frame][fcurve.array_index] = (
                        action_strip.evaluate_fcurve(fcurve, frame)
                    )

        for bl_attr, _, converter in light_node.attribute_conversion:
            if (bl_attr in ('color', 'shadow_color') and
                    bl_attr in color_frame_values_map):
                color_frame_values_map[bl_attr] = [
                    converter(x) for x in color_frame_values_map[bl_attr]
                ]

        for attribute, frame_value_list in color_frame_values_map.items():
            if attribute == 'color':
                track_path = base_node_path.new_copy('light_color')
            else:
                track_path = base_node_path.new_copy('shadow_color')

            anim_rsc.add_track(
                ColorTrack(
                    track_path,
                    frames_iter=range(first_frame, last_frame),
                    values_iter=frame_value_list
                )
            )


def export_camera_action(camera_node, export_settings, blender_cam,
                         action_strip, anim_rsc):
    """Export camera action"""
    first_frame, last_frame = action_strip.frame_range
    base_node_path = NodePath(
        anim_rsc.anim_player.parent.get_path(), camera_node.get_path()
    )

    fcurves = action_strip.action.fcurves
    for item in camera_node.attribute_conversion:
        bl_attr, gd_attr, converter = item
        anim_rsc.add_attribute_track(
            action_strip,
            fcurves.find(bl_attr),
            converter,
            base_node_path.new_copy(gd_attr),
            use_bezier=export_settings['feature_bezier_track'],
        )

    anim_rsc.add_attribute_track(
        action_strip,
        fcurves.find('type'),
        lambda x: 0 if x == 0.0 else 1,
        base_node_path.new_copy('projection'),
    )

    # blender use sensor_width and f_lens to animate fov
    # while godot directly use fov
    fov_animated = False
    focal_len_list = list()
    sensor_size_list = list()

    lens_fcurve = fcurves.find('lens')
    if lens_fcurve is not None:
        fov_animated = True
        for frame in range(first_frame, last_frame):
            focal_len_list.append(
                action_strip.evaluate_fcurve(lens_fcurve, frame)
            )
    sensor_width_fcurve = fcurves.find('sensor_width')
    if sensor_width_fcurve is not None:
        fov_animated = True
        for frame in range(first_frame, last_frame):
            sensor_size_list.append(
                action_strip.evaluate_fcurve(sensor_width_fcurve, frame)
            )

    if fov_animated:
        # export fov track
        if not focal_len_list:
            focal_len_list = [blender_cam.lens
                              for _ in range(first_frame, last_frame)]
        if not sensor_size_list:
            sensor_size_list = [blender_cam.sensor_width
                                for _ in range(first_frame, last_frame)]

        fov_list = list()
        for index, flen in enumerate(focal_len_list):
            fov_list.append(2 * math.degrees(
                math.atan(
                    sensor_size_list[index]/2/flen
                )
            ))

        anim_rsc.add_track(
            FloatTrack(
                base_node_path.new_copy('fov'),
                frames_iter=range(first_frame, last_frame),
                values_iter=fov_list
            )
        )
